﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics.Contracts;
using System.Text;
using SessionHunter.Business.Exceptions;
using SessionHunter.Model;

namespace SessionHunter.DataAccess
{
    /// <summary>
    /// This the the object responsible for all queries into the session
    /// table.  It does not do any caching, which is higher up the stack.
    /// Maps data from the database into objects.
    /// </summary>
    public class SessionRepository
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        #region Constants
        /// <summary>
        /// Used to populate the session ID if there is an error
        /// </summary>
        public const string ErrorSessionId = "<Error>";
        /// <summary>
        /// The SQL used to test the connection to the state database
        /// </summary>
        private const string _testConnectionSQL = "SELECT TOP 1 1 FROM ASPStateTempSessions";
        /// <summary>
        /// The SQL used to get the application data out
        /// </summary>
        private const string _applicationSQL = "SELECT AppId, AppName FROM ASPStateTempApplications";
        #endregion

        #region Variables
        private string _connectionString;
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionRepository"/> class.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        public SessionRepository(string connectionString)
        {
            Contract.Requires(!String.IsNullOrEmpty(connectionString), "Connection string cannot be null");

            _connectionString = connectionString;
        }

        /// <summary>
        /// Gets the current sessions out of the database.
        /// </summary>
        /// <param name="options">The current application options</param>
        /// <returns>
        /// A list of session.  Not null, but may be empty
        /// </returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public virtual IList<SessionData> GetCurrentSessions(ApplicationOptions options)
        {
            Contract.Requires(options != null);
            Contract.Ensures(Contract.Result<IList<SessionData>>() != null, "Null session list returned");
            Contract.Assert(options != null, "No application options passed in");
            Contract.Assert(!String.IsNullOrEmpty(_connectionString), "Connection string cannot be null");

            logger.Debug("GetCurrentSessions using connection string: " + _connectionString);

            // Always return a session list, it just may be empty
            var sessionList = new List<SessionData>();

            // Build the SQL query based on the parameters
            var sql = SqlBuilder(options);

            // Use a datareader (as fast as possible) and get the data out, hydrating the list of session objects
            SqlDataReader reader = null;
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
                try
                {
                    using (var command = new SqlCommand(sql.ToString(), connection))
                    {
                        connection.Open();
                        reader = command.ExecuteReader();
                        SessionData sessionData;
                        while (reader.Read())
                        {
                            // Get the session data out and create a session object with it.  Add it to the list we are returning
                            sessionData = HydrateSession(reader, options);
                            sessionList.Add(sessionData);
                        }
                    }
                }
                catch (SessionApplicationException)
                {
                    throw; // Already wrapped, just re-throw
                }
                catch (Exception ex)
                {
                    throw new SessionApplicationException("Data connection error", ex);
                }
                finally
                {
                    try
                    {
                        // Close reader/connection as safely as possible
                        if (reader != null && !reader.IsClosed)
                            reader.Close();

                        if (connection != null && connection.State != System.Data.ConnectionState.Closed)
                            connection.Close();
                    }
                    catch
                    {
                        // We tried to close the connection and failed.  Oh well, swallow the exception
                    }
                }

                // If we got the application ID, we need to fill in the application info
                if (options.Application && sessionList != null)
                {
                    var applicationList = GetApplicationData();
                    MapApplicationData(applicationList, sessionList);
                }
            }

            return sessionList;
        }

        /// <summary>
        /// Get the list of applications from the database
        /// </summary>
        /// <returns>A list of application</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        internal Dictionary<int, string> GetApplicationData()
        {
            Contract.Ensures(Contract.Result<Dictionary<int, string>>() != null, "Null session list returned");
            Contract.Assert(!String.IsNullOrEmpty(_connectionString), "Connection string cannot be null");

            logger.Info("Calling PopulateApplicationData");

            var applicationList = new Dictionary<int, string>();

            SqlDataReader reader = null;
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
                try
                {
                    using (var command = new SqlCommand(_applicationSQL, connection))
                    {
                        connection.Open();
                        reader = command.ExecuteReader();
                        while (reader.Read())
                        {
                            // Get the applications out and create a lookup.  At this point, we convert from hex to int
                            applicationList.Add(reader.GetInt32(0), reader.GetString(1).Trim());
                        }
                    }
                }
                finally
                {
                    try
                    {
                        // Close reader/connection as safely as possible
                        if (reader != null && !reader.IsClosed)
                            reader.Close();

                        if (connection != null && connection.State != System.Data.ConnectionState.Closed)
                            connection.Close();
                    }
                    catch
                    {
                        // We tried to close the connection and failed.  Oh well, swallow the exception
                    }
                }
            }

            return applicationList;
        }

        /// <summary>
        /// Takes all of the sessions and populates the application path/data
        /// </summary>
        /// <param name="applicationList">The application list.</param>
        /// <param name="sessionList">The session list.</param>
        internal void MapApplicationData(Dictionary<int, string> applicationList, List<SessionData> sessionList)
        {
            logger.Info("Mapping " + sessionList.Count + " sessions to " + applicationList.Count + " applications");
            string tempApp = null;
            int tempAppInt;
            foreach (var session in sessionList)
            {
                tempApp = session.Application;
                if (!String.IsNullOrEmpty(tempApp))
                {
                    // Convert to an int
                    tempAppInt = Convert.ToInt32(tempApp, 16);
                    // Find the application path in our lookup
                    if (applicationList.TryGetValue(tempAppInt, out tempApp))
                    {
                        // If we found one, overwrite the hex with the path
                        session.Application = tempApp;
                    }
                }
            }
        }


        /// <summary>
        /// Tests the current connection.  If there is an error, it will return the exception message
        /// </summary>
        /// <param name="connectionString">The connection string to test</param>
        /// <returns>
        /// Null or exception message
        /// </returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public static string TestConnection(string connectionString)
        {
            Contract.Assert(!string.IsNullOrEmpty(connectionString), "Connection string cannot be null");

            string exceptionMessage = null;

            logger.Debug("TestConnection using connection string: " + connectionString);

            // Use a datareader (as fast as possible) to test the query
            SqlDataReader reader = null;
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                try
                {
                    using (var command = new SqlCommand(_testConnectionSQL, connection))
                    {
                        connection.Open();
                        reader = command.ExecuteReader();
                        while (reader.Read())
                        {
                            // Do nothing but read data.  We don't care what comes back, we just care there are no errors
                        }
                    }
                }
                catch (Exception ex)
                {
                    logger.Error("TestConnection raised an error", ex);
                    exceptionMessage = ex.Message;
                }
                finally
                {
                    try
                    {
                        // Close reader/connection as safely as possible
                        if (reader != null && !reader.IsClosed)
                            reader.Close();

                        if (connection != null && connection.State != System.Data.ConnectionState.Closed)
                            connection.Close();
                    }
                    catch
                    {
                        // We tried to close the connection and failed.  Oh well, swallow the exception
                    }
                }
            }

            return exceptionMessage;
        }

        /// <summary>
        /// Builds the SQL query used to get sessions
        /// </summary>
        /// <param name="options">The options set on the admin screen</param>
        /// <returns>A SQL string</returns>
        /// <remarks>Column order is important for HydrateSession</remarks>
        private static StringBuilder SqlBuilder(ApplicationOptions options)
        {
            Contract.Requires(options != null);
            Contract.Ensures(Contract.Result<StringBuilder>() != null, "Null session list returned");
            Contract.Assert(options != null, "No application options passed in");

            var sql = new StringBuilder();

            sql.Append("SELECT ");
            // Limit the number of sessions
            if (options.NumberOfSessions != ApplicationOptions.ShowAllSessionsValue)
                sql.AppendFormat(" TOP {0} ", options.NumberOfSessions);

            //  Add standard columns
            var tempString = @"SessionId,
CASE
     WHEN SessionItemShort is not null THEN SessionItemShort
     ELSE SessionItemLong
END as Data,
CASE
     WHEN SessionItemShort is not null THEN Datalength(SessionItemShort) / 1048576.0
     ELSE Datalength(SessionItemLong) / 1048576.0
END as Size";
            sql.Append(tempString);

            // Create/Expires data
            if (options.ShowCreateExpires)
                sql.Append(",Created, Expires");

            // Lock info
            if (options.ShowLockInfo)
                sql.Append(", LockDate, LockDateLocal, Timeout, Locked");

            // Get the AppID as a hex value
            if (options.Application)
                sql.Append(", substring(SessionId,25,8) as AppIDHex");

            // Defines which table to pull from
            sql.Append(" FROM ASPStateTempSessions ORDER BY ");

            // What to order the results by 
            switch (options.OrderBy)
            {
                case ApplicationOptions.OrderByParameter.SessionId:
                    sql.Append("SessionId");
                    break;
                case ApplicationOptions.OrderByParameter.LargestSession:
                    sql.Append("Size DESC");
                    break;
                case ApplicationOptions.OrderByParameter.CreateDateLeastRecent:
                    sql.Append("Created ASC, Size DESC");
                    break;
                case ApplicationOptions.OrderByParameter.CreateDateMostRecent:
                    sql.Append("Created DESC, Size DESC");
                    break;
                default:
                    throw new UnhandledCaseException(options.OrderBy);
            }

            logger.Info("SessionRepository SQL: " + sql);
            return sql;
        }

        /// <summary>
        /// Hydrates a session object with data we got from the database.
        /// </summary>
        /// <param name="reader">The reader with the data.</param>
        /// <param name="options">The current application options</param>
        /// <returns>
        /// A hydrated object or null if there was an issue
        /// </returns>
        private static SessionData HydrateSession(SqlDataReader reader, ApplicationOptions options)
        {
            Contract.Requires(reader != null, "Reader passed to HydrateSession is null");
            Contract.Requires(options != null, "No application options passed in");
            Contract.Ensures(Contract.Result<SessionData>() != null, "Null session data returned");

            SessionData session = null;

            // Get all the data out of the reader and map it
            try
            {
                var sessionId = (string)reader["SessionId"];
                if (!String.IsNullOrEmpty(sessionId))
                {
                    var size = (Decimal)reader["Size"];

                    // Create the session object we are going to hydrate
                    session = new SessionData(sessionId, size);
                    session.Data = (Byte[])reader["Data"];
                    if (options.ShowCreateExpires)
                    {
                        session.Created = (DateTime)reader["Created"];
                        session.Expires = (DateTime)reader["Expires"];
                    }
                    if (options.ShowLockInfo)
                    {
                        session.LockDate = (DateTime)reader["LockDate"];
                        session.LockDateLocal = (DateTime)reader["LockDateLocal"];
                        session.Timeout = (int)reader["Timeout"];
                        session.Locked = (bool)reader["Locked"];
                    }
                    if (options.Application)
                    {
                        session.Application = (string)reader["AppIDHex"];
                    }
                }
            }
            catch (Exception ex)
            {
                throw new SessionApplicationException("Error mapping retrieved data to session object", ex);
            }

            // Return the session we mapped or an object with an error flag
            if (session != null)
                return session;
            else
                return new SessionData(ErrorSessionId, 0);
        }

        /// <summary>
        /// Makes sure the object is in a good state overall
        /// </summary>
        [ContractInvariantMethod]
        void ObjectInvariant()
        {
            Contract.Invariant(!String.IsNullOrEmpty(_connectionString));
        }

    }
}
